Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ 3주차 기본/심화 과제 ] 3주차 과제 제출 #5

Open
wants to merge 46 commits into
base: main
Choose a base branch
from

Conversation

Geun-Oh
Copy link
Contributor

@Geun-Oh Geun-Oh commented Apr 30, 2023

✨ 구현 기능 명세

기본 과제

✅ 게임 난이도 선택

  1. 난이도의 종류
    1. easy → 10개 :: 5쌍 맞추기
    2. normal → 14개 :: 7쌍 맞추기
    3. hard → 18개 :: 9쌍 맞추기
  2. 난이도 중간에 바꿀시, 카드 모두 뒤집어서 처음으로 돌아가기

=> 구현 완료했습니다!

✅ 정답 수 노출

  1. (현재 나의 스코어) / 전체 정답 으로 상단에 노출
{correctCard.length} / {difficulty === 'Easy' ? 5 : difficulty === 'Normal' ? 7 : 9}

=> 구현 완료했습니다!

✅ 카드 선택

  1. 2개의 카드를 선택하면 다른 카드는 선택할 수 없습니다.
  2. 해당 카드의 일치 여부를 조사
    1. 정답일 경우
      1. 정답 스코어 증가
      2. 카드 뒤집힌 채로 유지
    2. 오답일 경우
      1. 카드 다시 뒷면으로 돌리기

=> 열린 카드 / 쌍을 맞춘 카드를 구분하여 구현 완료했습니다!

✅ 카드 배열 순서

  1. 카드가 배열되는 순서는 반드시 랜덤으로 지정
  2. 난이도에 따라 지정되는 쌍도 랜덤으로 지정

=> src/utils/getRandomIndexArray.js 를 통해 구현 완료했습니다!

심화 과제

✅ 애니메이션

  1. 카드를 선택
    1. 뒤집어지는 기깔나는 애니메이션을 적용해주세요!!
  2. 카드 쌍을 맞춘 경우
    1. 저는 현재 스코어가 빛나는 애니메이션을 적용했습니다! 마음대루!!

=> 막 엄청 예쁜 애니메이션은 아니지만...구현 완료했습니다!

✅ theme + styled-components :: 적용

  1. globalstyle
  2. theme
    1. 전역에서 사용할 수 있도록 적용해보세요!

=> 적용 완료했습니다!

✅ 게임 초기화 버튼

  1. 게임 도중 ‘다시하기’ 버튼을 누르면 모든 게임 설정이 처음으로 돌아갑니다.

=> 구현 완료했습니다!

✅ createPortal

  1. 모든 카드 맞추기가 끝난 후 보여주는 모달을 Portal을 활용해 만들어주세요!

=> 구현 완료했습니다!


🌼 PR Point

Context API 활용

props drilling을 해야하나...말아야하나... 고민을 많이 했는데, 파트장님의 의도는 props drilling을 열심히 해보라는 것일 수도 있는데,,,
결국 중요한 건 전역 상태와 props drilling을 어느 단에서 얼마나 적용할지 잘 생각해보고 절충해서 적용하는 것이라고 생각합니다...
그래서 useContext 사..용 했습니다...이건 react 내장 훅이니까 괜찮죠..? 호호

src/context/reducer.js

import { createContext, useContext } from 'react';

export const initialContext = {
  openCard: [],
  correctCard: [],
  difficulty: 'Easy',
  randomArray: [],
  successModalOpen: false,
  addOpenCard: () => {},
  clearOpenCard: () => {},
  addCorrectCard: () => {},
  clearCorrectCard: () => {},
  setDifficulty: () => {},
  setRandomArray: () => {},
  setSuccessModalOpen: () => {},
};

export const Context = createContext(initialContext);

Context.displayName = 'Context';

export const useGlobalContext = () => {
  const context = useContext(Context);

  if (context === undefined) {
    throw new Error('useGlobalContext must be used within a <ContextProvider />');
  }
  return context;
};

export const reducer = (state, action) => {
  switch (action.type) {
    case 'ADD_OPENCARD':
      return {
        ...state,
        openCard: [...state.openCard, action.value],
      };
    case 'CLEAR_OPENCARD':
      return {
        ...state,
        openCard: [],
      };
    case 'ADD_CORRECTCARD':
      return {
        ...state,
        correctCard: [...state.correctCard, action.value],
      };
    case 'CLEAR_CORRECTCARD':
      return {
        ...state,
        correctCard: [],
      };
    case 'SET_DIFFICULTY':
      return {
        ...state,
        difficulty: action.value,
      };
    case 'SET_RANDOMARRAY':
      return {
        ...state,
        randomArray: action.value,
      };
    case 'SET_SUCCESSMODALOPEN':
      return {
        ...state,
        successModalOpen: !state.successModalOpen,
      };
    default:
      return state;
  }
};

전역으로 관리할 초기 상태와 상태를 변경할 수 있는 함수를 context로 생성해주고, reducer를 생성하여 상태 변경 로직을 담았습니다.

이때, useGlobalContext 커스텀 훅을 선언하여 생성한 context가 Provider 내부에 존재하는지 검증해주었습니다.

src/context/provider.jsx

import { useReducer } from 'react';

import { Context, initialContext, reducer } from './reducer';

export const ContextProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialContext);
  return (
    <Context.Provider
      value={{
        ...state,
        addOpenCard: (value) => dispatch({ type: 'ADD_OPENCARD', value }),
        addCorrectCard: (value) => dispatch({ type: 'ADD_CORRECTCARD', value }),
        clearOpenCard: () => dispatch({ type: 'CLEAR_OPENCARD' }),
        clearCorrectCard: () => dispatch({ type: 'CLEAR_CORRECTCARD' }),
        setDifficulty: (value) => dispatch({ type: 'SET_DIFFICULTY', value }),
        setRandomArray: (value) => dispatch({ type: 'SET_RANDOMARRAY', value }),
        setSuccessModalOpen: () => dispatch({ type: 'SET_SUCCESSMODALOPEN' }),
      }}>
      {children}
    </Context.Provider>
  );
};

전역 상태를 전달해줄 provider를 선언하고 value 값으로 초기 상태와 각 상태 변경 로직들을 담아주었습니다.


3d 애니메이션 적용

src/template/CardTemplate.jsx

const StyledCardWrapper = styled.main`
  perspective: 1200px;
`

카드 전체를 덮어주는 wrapper에서 perspective 속성을 통해 원근감을 적용했습니다.

src/components/atom/CardView.jsx

const CardView = ({ rotate, onClick, imgURL }) => {
  return (
    // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
    <StyledCardArticle rotate={rotate} onClick={onClick}>
      <div className="card_frontface">
        <img src={imgURL} alt={`찌호이미지: ${imgURL}`} />
      </div>
      <div className="card_backface"></div>
    </StyledCardArticle>
  );
};

const StyledCardArticle = styled.article`
  position: relative;
  transform: ${({ rotate }) => rotate};
  transition-duration: 1s;
  transform-style: preserve-3d;
  .card_frontface {
    width: 180px;
    height: 250px;
    border-radius: 20px;
    background-color: white;
    display: flex;
    justify-content: center;
    align-items: center;
    img {
      width: 150px;
      backface-visibility: hidden;
    }
  }
  .card_backface {
    top: 0;
    left: 0;
    width: 180px;
    height: 250px;
    border-radius: 20px;
    background-color: #a11818;
    position: absolute;
    transform: rotateY(180deg);
    backface-visibility: hidden;
  }
`;

각각의 카드 내부를 앞면, 뒷면으로 나눠주었고, 앞뒷면을 감싸는 컴포넌트에 transform-style: preserve-3d; 속성을 적용해주어 각 perspective 속성이 카드 내부 요소로 전달될 수 있도록 해주었습니다.

또한 내부 카드들 중 뒷면에는 기본적으로 rotateY(180deg)를 적용하고, 앞뒷면 모두에 backface-visibility: hidden;을 적용하여 카드의 뒤집히는 애니메이션을 구현하였습니다.


타입 별 버튼 렌더링

src/styles/theme.js

const CommonButtonStyle = `
border: none;
border-radius: 5px;
display: flex;
justify-content: center;
align-items: center;
transition-duration: 0.3s;
box-shadow: 0px 1px 5px rgba(0, 0, 0, 0.1), 0px 3.6px 13px rgba(0, 0, 0, 0.07), 0px 8.4px 23px rgba(0, 0, 0, 0.06), 0px 23px 35px rgba(0, 0, 0, 0.05), 0px 4.5px 10px rgba(0, 0, 0, 0.05);
:hover {
  color: white;
  background-color: #a11818;
}
`;

const theme = {
  Background: '#F1C85E',
  SubBackground: '#ebccb0',
  Text: '#a11818',
  CardShadow:
    '0px 1px 5px rgba(0, 0, 0, 0.1), 0px 3.6px 13px rgba(0, 0, 0, 0.07), 0px 8.4px 23px rgba(0, 0, 0, 0.06), 0px 23px 35px rgba(0, 0, 0, 0.05), 0px 4.5px 10px rgba(0, 0, 0, 0.05);',
  CommonButtonStyle,
};

공통으로 사용되는 버튼 스타일을 지정하여 ThemeProvider를 통해 전달될 수 있도록 했습니다.

src/components/atom/ButtonView.jsx

const StyledDifficulfyButton = styled.button`
  ${({ theme }) => theme.CommonButtonStyle}
  height: 60px;
  padding: 10px 20px;
  font-size: 1.5rem;
  border: none;
  border-radius: 5px;
  color: #6b2019;
  background-color: #d37e75;
  transition-duration: 0.3s;
`;

이후 버튼에서 공용 버튼 스타일을 적용해주었습니다.

src/components/ButtonView.jsx

const buttonTypes = {
  DIFFICULTY: StyledDifficulfyButton,
  RESET: StyledResetButton,
  SUCCESSMODAL: StyledSuccessModalButton,
};

const ButtonView = ({ type, innerText, onClick }) => {
  const ButtonType = buttonTypes[type];
  return (
    <ButtonType type="button" onClick={onClick} value={innerText}>
      {innerText}
    </ButtonType>
  );
};
export default ButtonView;

buttonType을 통해 if 나 switch문을 사용하는 대신 하나의 컴포넌트 내에서 타입지정을 통한 렌더링 분기를 적용했습니다.


짝 맞출 시 애니메이션 적용

src/styles/bright.css

.bright {
  animation: bright cubic-bezier(0.82, 1.48, 0, 0.77) 0.8s;
}
@keyframes bright {
  0% {
    transform: scale(1);
    color: #6b3a35;
  }
  50% {
    transform: scale(1.8);
    color: #a11818;
    font-weight: 700;
    top: -20px;
    text-shadow: 0 1px 0 #ccc, 0 2px 0 #ccc, 0 3px 0 #ccc, 0 4px 0 #ccc, 0 5px 0 #ccc, 0 6px 0 #ccc, 0 7px 0 #ccc,
      0 8px 0 #ccc, 0 9px 0 #ccc, 0 50px 25px rgba(0, 0, 0, 0.2);
  }
  100% {
    transform: scale(1);
    color: #6b3a35;
  }
}

애니메이션을 생성한 뒤 클래스를 통해 관리할 수 있도록 해주었습니다.

src/components/Header.jsx

const Header = () => {
  const { correctCard, difficulty } = useGlobalContext();
  const spanRef = useRef();

  useEffect(() => {
    spanRef.current.classList.add('bright');
    setTimeout(() => {
      spanRef.current.classList.remove('bright');
    }, 800);
  }, [correctCard]);

  return (
    <StyledHeader>
      <h1>⭐찌호를 맞춰주세요⭐</h1>
      <span ref={spanRef}>
        {correctCard.length} / {difficulty === 'Easy' ? 5 : difficulty === 'Normal' ? 7 : 9}
      </span>
    </StyledHeader>
  );
};

이후 점수 표기하는 곳에 useRef를 사용하여 class를 탈부착 하는 형식으로 특정 상황에서 애니메이션이 작동할 수 있도록 구현해주었습니다.


🥺 소요 시간, 어려웠던 점

  • 8h

  • 전체적으로 오랜 시간이 걸렸고, 초반에 일단 기능을 구현하는데 힘쓰고 이후에 리팩토링 하는 과정에서 디렉토리 구조를 미리 설계할걸 하는 후회가 조금 있었습니다. 이후에는 미리 생각하고 만드는 J 스러운 면모를 가지도록..노력해보겠습니다.

  • 첫 리액트 과제였는데, 오랜만에 다양한 리액트 훅을 사용하니 리액트의 감사함을 많이 느낀 것 같아요. 물론 그만큼 훅 없이도 기능들을 구현할 수 있는 사람이 되어야 하겠지만요!

  • 언제나 고민할 수 있는 포인트를 제공해주어 성장에 큰 발판이되는 과제들인 것 같습니다. 항상 질 좋은 과제 제공해주셔서 감사합니다 :)


🌈 구현 결과물

3주차 과제

@Geun-Oh Geun-Oh self-assigned this Apr 30, 2023
Copy link

@Yeonseo-Jo Yeonseo-Jo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

context API도 활용하고 theme도 야무지게 잘 사용했다! 코드가 전체적으로 다 깔끔하넹
항상 형근이 코드 보고 많이 배워가💪🏻!!
다음주차 과제도 기대할겡 아자자!

Comment on lines +11 to +21
const ImgChunks = {
IMG_1,
IMG_2,
IMG_3,
IMG_4,
IMG_5,
IMG_6,
IMG_7,
IMG_8,
IMG_9,
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이미지들 관리해주는 방식 좋다!

<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요기서 상단바 아이콘도 바꿔주면 좋을거 가태


const Header = () => {
const { correctCard, difficulty } = useGlobalContext();
const spanRef = useRef();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useRef 사용 야무지다✨

<StyledHeader>
<h1>⭐찌호를 맞춰주세요⭐</h1>
<span ref={spanRef}>
{correctCard.length} / {difficulty === 'Easy' ? 5 : difficulty === 'Normal' ? 7 : 9}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나는 요거 switch문 갈겼는데 삼항연산자 사용하니 넘 깔꼼 ,,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사실 switch 쓰는 것도 괜찮을지도!! 가독성은 둘 다 비슷한 것 같아


const StyledHeader = styled.header`
width: 100vw;
height: 200px;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요기만 px 사용한 이유는 뭘깡 ?! 궁금해용

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아아마...
rem은 최상위 요소 font-size를 따라가니까 어찌되었든 가변성이 있는 값이잖아!!!

그래서 header나 nav 같이 특정 고정 값들이 필요하다고 생각되는 곳에는 픽셀을 적는 것 같아!

};
const rotate = correctCard.includes(url) || openCard.some((x) => x.index === index);

return <CardView rotate={rotate ? 'rotateY(0deg)' : 'rotateY(180deg)'} onClick={buttonOnclick} imgURL={imgURL} />;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

뒤집어지는 효과 삼항 연산자로 걸어준거 예쁘다✨

Comment on lines +3 to +5
const ModalRenderer = ({ children }) => {
return ReactDom.createPortal(children, document.querySelector('#modal'));
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

children 사용하니까 더 명확하당

Comment on lines +31 to +47
export const reducer = (state, action) => {
switch (action.type) {
case 'ADD_OPENCARD':
return {
...state,
openCard: [...state.openCard, action.value],
};
case 'CLEAR_OPENCARD':
return {
...state,
openCard: [],
};
case 'ADD_CORRECTCARD':
return {
...state,
correctCard: [...state.correctCard, action.value],
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useContext랑 reducer는 일케 사용하는거구낭 형근이 코드 보구 많이 배운다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅋㅎㅋㅋ쿠ㅜ 리덕스도 이거랑 거의 구조가 비슷하니까 한 번 익혀두면 써먹기 좋아!! 나는 그랬어..

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7주차에 드디어 형근이의 코드를 이해할 수 있다ㅋㅋㅋㅋ

Comment on lines +11 to +21
const getArray = () => {
switch (difficulty) {
case 'Easy':
return shuffle(easyArr);
case 'Normal':
return shuffle(normalArr);
case 'Hard':
return shuffle(hardArr);
default:
return [];
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

가독성 구웃

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드 진짜 짱 깔끔,,

Comment on lines +6 to +9
export default defineConfig({
plugins: [react(), image()],
// base: '/HyeongGeunOh/',
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요게 그 vite 배포 중 생긴 오류 땜에 붙인 코드인가유?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

녜...근데도 성공 못한....

Copy link

@gunom gunom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

또 많이 많이 배워갑니다

const context = useContext(Context);

if (context === undefined) {
throw new Error('useSelect must be used within a <ContextProvider />');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

햐 이런 에러 핸들링 너무 좋다

Comment on lines +11 to +21
const getArray = () => {
switch (difficulty) {
case 'Easy':
return shuffle(easyArr);
case 'Normal':
return shuffle(normalArr);
case 'Hard':
return shuffle(hardArr);
default:
return [];
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드 진짜 짱 깔끔,,

const difficultyHandler = (e) => {
setDifficulty(e.target.value);
clearOpenCard();
clearCorrectCard();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이벤트 핸들러로 난이도 설정하고 clear opencard, correct card 깔끔하다!

setTimeout(() => {
spanRef.current.classList.remove('bright');
}, 800);
}, [correctCard]);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useEffect로 spanRef에 bright클래스 추가하고 다시 제거하는 로직이구나 멋지다!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants